iT邦幫忙

2024 iThome 鐵人賽

DAY 24
0
Modern Web

React 學得動嗎系列 第 24

[Day 24] Gym Pro:強化安全性與優化用戶體驗

  • 分享至 

  • xImage
  •  

今天,我們要來為我們的系統加上重要的安全機制,並且優化整體的用戶體驗。

1. 實現身份驗證

首先,我們要實現一個完整的身份驗證機制。我們將使用 JWT(JSON Web Token)來進行身份驗證。

安裝必要的套件

npm install jsonwebtoken axios
npm install -D @types/jsonwebtoken

建立 AuthContext

src/contexts/AuthContext.tsx 中:

import React, { createContext, useState, useContext, useEffect } from 'react';
import axios from 'axios';

interface User {
  id: number;
  username: string;
  role: string;
}

interface AuthContextType {
  user: User | null;
  login: (username: string, password: string) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    const token = localStorage.getItem('token');
    if (token) {
      axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
      // 這裡可以添加驗證 token 有效性的邏輯
    }
  }, []);

  const login = async (username: string, password: string) => {
    try {
      const response = await axios.post('/api/login', { username, password });
      const { user, token } = response.data;
      localStorage.setItem('token', token);
      axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
      setUser(user);
    } catch (error) {
      console.error('登入失敗', error);
      throw error;
    }
  };

  const logout = () => {
    localStorage.removeItem('token');
    delete axios.defaults.headers.common['Authorization'];
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth 必須在 AuthProvider 內使用');
  }
  return context;
};

更新 Login 頁面

src/pages/Login.tsx 中:

import React from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"

interface LoginForm {
  username: string;
  password: string;
}

const Login: React.FC = () => {
  const { register, handleSubmit } = useForm<LoginForm>();
  const navigate = useNavigate();
  const { login } = useAuth();

  const onSubmit = async (data: LoginForm) => {
    try {
      await login(data.username, data.password);
      navigate('/dashboard');
    } catch (error) {
      console.error('登入失敗', error);
      // 這裡可以添加錯誤處理,比如顯示錯誤訊息
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100">
      <Card className="w-[350px]">
        <CardHeader>
          <CardTitle>歡迎來到 Gym Pro</CardTitle>
          <CardDescription>請登入以繼續</CardDescription>
        </CardHeader>
        <CardContent>
          <form onSubmit={handleSubmit(onSubmit)}>
            <div className="space-y-4">
              <Input {...register('username')} placeholder="使用者名稱" required />
              <Input {...register('password')} type="password" placeholder="密碼" required />
            </div>
            <Button className="w-full mt-4" type="submit">登入</Button>
          </form>
        </CardContent>
      </Card>
    </div>
  );
};

export default Login;

2. 實現基於角色的存取控制(RBAC)

接下來,我們要實現基於角色的存取控制,確保只有具有適當權限的用戶才能訪問特定頁面或執行特定操作。

建立 PrivateRoute 組件

src/components/PrivateRoute.tsx 中:

import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';

interface PrivateRouteProps {
  children: React.ReactNode;
  requiredRole?: string;
}

const PrivateRoute: React.FC<PrivateRouteProps> = ({ children, requiredRole }) => {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  if (requiredRole && user.role !== requiredRole) {
    return <Navigate to="/unauthorized" replace />;
  }

  return <>{children}</>;
};

export default PrivateRoute;

更新路由

src/App.tsx 中:

import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import PrivateRoute from './components/PrivateRoute';
import MainLayout from './layouts/MainLayout';
import Dashboard from './pages/Dashboard';
import Members from './pages/Members';
import MemberDetail from './pages/MemberDetail';
import Classes from './pages/Classes';
import ClassCalendar from './pages/ClassCalendar';
import ClassDetail from './pages/ClassDetail';
import Reports from './pages/Reports';
import Login from './pages/Login';
import Unauthorized from './pages/Unauthorized';

function App() {
  return (
    <AuthProvider>
      <Router>
        <Routes>
          <Route path="/login" element={<Login />} />
          <Route path="/unauthorized" element={<Unauthorized />} />
          <Route path="/" element={<PrivateRoute><MainLayout /></PrivateRoute>}>
            <Route index element={<Dashboard />} />
            <Route path="members" element={<PrivateRoute requiredRole="admin"><Members /></PrivateRoute>} />
            <Route path="members/:id" element={<PrivateRoute requiredRole="admin"><MemberDetail /></PrivateRoute>} />
            <Route path="classes" element={<Classes />} />
            <Route path="class-calendar" element={<ClassCalendar />} />
            <Route path="classes/:id" element={<PrivateRoute requiredRole="trainer"><ClassDetail /></PrivateRoute>} />
            <Route path="reports" element={<PrivateRoute requiredRole="admin"><Reports /></PrivateRoute>} />
          </Route>
        </Routes>
      </Router>
    </AuthProvider>
  );
}

export default App;

3. 全局錯誤處理

為了提供更好的用戶體驗,我們應該實現全局錯誤處理。

建立 ErrorBoundary 組件

src/components/ErrorBoundary.tsx 中:

import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  children: ReactNode;
}

interface State {
  hasError: boolean;
}

class ErrorBoundary extends Component<Props, State> {
  public state: State = {
    hasError: false
  };

  public static getDerivedStateFromError(_: Error): State {
    return { hasError: true };
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('Uncaught error:', error, errorInfo);
  }

  public render() {
    if (this.state.hasError) {
      return <h1>抱歉,出了點問題。請稍後再試。</h1>;
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

在 App 組件中使用 ErrorBoundary

更新 src/App.tsx

import ErrorBoundary from './components/ErrorBoundary';

function App() {
  return (
    <ErrorBoundary>
      <AuthProvider>
        {/* ... 其他代碼 ... */}
      </AuthProvider>
    </ErrorBoundary>
  );
}

4. 優化用戶界面

最後,讓增加一些小細節來改善整體的用戶體驗。

增加載入指示器

建立一個 src/components/LoadingSpinner.tsx

import React from 'react';

const LoadingSpinner: React.FC = () => (
  <div className="flex justify-center items-center h-screen">
    <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900"></div>
  </div>
);

export default LoadingSpinner;

在需要載入數據的組件中使用這個 LoadingSpinner。

增加反饋訊息

使用 react-toastify 來增加反饋訊息:

npm install react-toastify

src/App.tsx 中:

import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

function App() {
  return (
    <ErrorBoundary>
      <AuthProvider>
        {/* ... 其他代碼 ... */}
        <ToastContainer />
      </AuthProvider>
    </ErrorBoundary>
  );
}

然後在需要顯示反饋訊息的地方使用 toast 函數。

小結

今天我們為 Gym Pro 系統增加了安全機制和用戶體驗改進,包括:

  1. 實現了基於 JWT 的身份驗證系統。
  2. 增加了基於角色的存取控制(RBAC)。
  3. 實現了全局錯誤處理機制。
  4. 優化了用戶界面,增加了載入指示器和反饋訊息。

上一篇
[Day 23] Gym Pro:打造報表和分析功能
下一篇
[Day 25] Gym Pro:會員積分系統與課程預約功能
系列文
React 學得動嗎30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言